<?php

// vim: expandtab shiftwidth=2 softtabstop=2 tabstop=2

/**
 * Simplifier access to db via PDO
 *
 * PHP version 5
 *
 * @category  Classes
 * @package   mPack
 * @author	  Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mpdoeasy.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */

/**
 * mPDOEasy exception class
 *
 * @category  Classes
 * @package   mPack
 * @author	  Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mpdoeasy.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */
class mPDOEasyError extends PDOException {}

/**
 * mPDOEasy interface
 *
 * @category  Classes
 * @package   mPack
 * @author	  Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mpdoeasy.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */
interface ImPDOEasy {
	public function execute ();
	public function single ();
	public function set_character_set ( $set );
	public function insert_id ();
	public function quote ( $arg );
	public function database ( $dbname );
	public function begin ();
	public function commit ();
	public function rollback ();
	public function fetch_mode ( $fetchmode = null );
	public function get_pdo ();
	public function cache ();
	public function invalidate ();
	public function debug_mode ( $mode = null );
}

/**
 * Simplifier access to db via PDO
 *
 * @category  Classes
 * @package   mPack
 * @author	  Marcin Sztolcman <marcin /at/ urzenia /dot/ net>
 * @copyright 2006 Marcin Sztolcman
 * @license   GPL v.2
 * @version   SVN: $Id: class_mpdoeasy.php 29 2008-01-20 23:15:20Z urzenia $
 * @link      http://m-pack.googlecode.com
 */
class mPDOEasy implements ImPDOEasy {
	/**
	 * PDO handler
	 *
	 * @var object
	 * @access protected
	 */
	protected $db			= null;

	/**
	 * PDO statement (cursor)
	 *
	 * @var object
	 * @access protected
	 */
	protected $cursor		= null;

	/**
	 * PDO fetch mode (posibbly values: see PDO::FETCH_*)
	 *
	 * @var object
	 * @access protected
	 */
	protected $fetchmode	= PDO::FETCH_ASSOC;

	/**
	 * Database connection data
	 *
	 * @var object
	 * @access protected
	 */
	protected $dbdata		= array ();

	/**
	 * Cache array
	 *
	 * @var object
	 * @access protected
	 */
	protected $cache		= array ();

	/**
	 * Debug mode enabled or disabled
	 *
	 * @var bool
	 * @access protected
	 */
	protected $debug		= false;

  /**
   * Constructor.
   *
   * If filename in argument is specfified, it opens file and read them.
   * Assign available filters too (if any).
   *
   * Dsn provided as first parameter must be in format:
   * dbtype://login:password@host/dbname
   * For example:
   * mysql://mylogin:mypasswd@localhost/test
   *
   * @param string	$dsn
   *
   * @access public
   */
	public function __construct ( $dsn = null ) {
		if ( !preg_match ( '#^(mysql|psql)://([^:]+):([^@]+)@([^/]+)/(.*)$#', $dsn, $data ) ) {
			throw new mPDOEasyError ( sprintf ( 'Invalid DSN >%s<', $dsn ) );
		}

		$this->dbdata = array (
			'host' => $data[4],
			'user' => $data[2],
			'pass' => $data[3],
			'name' => $data[5],
			'type' => $data[1],
		);

		$this->connect ();
	}

  /**
   * Connect to database
   *
   * @return bool
   * @throws mPDOEasyError
   * @access protected
   */
	protected function connect () {
		try {
			$this->db = null;
			$this->db = new PDO ( sprintf ( '%s:host=%s;dbname=%s',
					$this->dbdata['type'],
					$this->dbdata['host'],
					$this->dbdata['name']
				),
				$this->dbdata['user'],
				$this->dbdata['pass']
			);
		}
		catch ( PDOException $e ) {
			throw new mPDOEasyError ( sprintf ( 'Błąd połączenia z bazą danych >%s://%s:#####@%s/%s<.',
				$this->dbdata['type'],
				$this->dbdata['user'],
				$this->dbdata['host'],
				$this->dbdata['name']
			) );
		}

		return true;
	}

  /**
   * Prepare and execute SQL query.
   *
   * If passed, it will bind parameters to sql with escaping sensitive characters.
   * Parameters can be passed as single value (string, integer etc), and then it
   * is converted to string. If it is passed as an array, it's first parameter is
   * value which is passed to query, and second must be one of PDO::PARAM_*.
   * More in PDOStatement::bindValue() documentation.
   *
   * @param array	$args	array of parameters, where index 0 is for SQL query, and next are values to bind to it.
   *
   * @return string	query passed as first parameter
   * @throws PDOException
   * @access protected
   */
	protected function prepare ( &$args ) {
		$query	= array_shift ( $args );

		# close cursor if not close before
		if ( $this->cursor ) {
			$this->cursor->closeCursor ();
		}

		# we create ne cursor
		$this->cursor = $this->db->prepare ( $query );

		# bind parameters
		foreach ( $args as $pos => $arg ) {
			if ( is_array ( $arg )) {
				$this->cursor->bindValue ( $pos+1, $arg[0], $arg[1] );
			}
			else {
				$this->cursor->bindValue ( $pos+1, $arg );
			}
		}

		# set fetch mode (default: PDO::FETCH_ASSOC)
		$this->cursor->setFetchMode ( $this->fetchmode );

		# execute
		$this->cursor->execute ();

		# return used query
		return $query;
	}

  /**
   * Execute query
   *
   * If passed, it will bind parameters to sql with escaping sensitive characters.
   * Parameters can be passed as single value (string, integer etc), and then it
   * is converted to string. If it is passed as an array, it's first parameter is
   * value which is passed to query, and second must be one of PDO::PARAM_*.
   * More in PDOStatement::bindValue() documentation.
   *
   * Return value is dependent of query type. If SELECT or SHOW, returns array
   * with query result. If UPDATE, DELETE or REPLACE, return quant of affected
   * rows. On INSERT, return last insert id for auto_increment id. In every other
   * case, returns true or false.
   *
   * @param array	$args	array of parameters, where index 0 is for SQL query, and next are values to bind to it.
   *
   * @return mixed
   * @throws PDOException
   * @access public
   */
	public function execute () {
		$args = func_get_args ();

		# wykonujemy zapytanie
		$query = $this->prepare ( $args );

		# sprawdzamy rodzaj zapytania
		if ( !preg_match ( '#\s*([a-z]+)\s+#smi', $query, $query_type ) ) {
			$query_type = false;
		}
		else {
			$query_type = strtolower ( $query_type[1] );
		}

		# domyślnie zwracamy prawdę
		$ret = true;
		switch ( $query_type ) {
			case 'show':
			case 'select':
				# zwracamy wszystkie wyniki
				$ret = $this->cursor->fetchAll ();
				break;

			case 'update':
			case 'delete':
			case 'replace':
				# zwracamy liczbę afektowanych pól
				$ret = $this->cursor->rowCount ();
				break;

			case 'insert':
				# zwracamy ostatnio dodane ID
				$ret = $this->db->lastInsertId ();
				break;
		}

		# zamykamy kursor
		$this->cursor->closeCursor ();

		# zwracamy wynik
		return $ret;
	}

  /**
   * Execute query and return single row.
   *
   * If passed, it will bind parameters to sql with escaping sensitive characters.
   * Parameters can be passed as single value (string, integer etc), and then it
   * is converted to string. If it is passed as an array, it's first parameter is
   * value which is passed to query, and second must be one of PDO::PARAM_*.
   * More in PDOStatement::bindValue() documentation.
   *
   * @param array	$args	array of parameters, where index 0 is for SQL query, and next are values to bind to it.
   *
   * @return array
   * @throws PDOException
   * @access public
   */
	public function single () {
		$args = func_get_args ();
		$this->prepare ( $args );
		$ret = $this->cursor->fetch ();
		$this->cursor->closeCursor ();

		return $ret;
	}

  /**
   * Set fetch mode.
   *
   * @param int   $fetchmode	One of PDO::FETCH_*
   *
   * @return bool always true
   * @access public
   */
	public function fetch_mode ( $fetchmode = null ) {
		if ( is_null ( $fetchmode ) ) {
			return $this->fetchmode;
		}
		else {
			$this->fetchmode = $fetchmode;
		}

		return true;
	}

  /**
   * Set character set used with SQL connection.
   *
   * For MySQL only.
   *
   * @param string	 $set	character set passed to SET NAMES MySQL query.
   * @return bool always true
   * @throws PDOException
   * @access public
   */
	public function set_character_set ( $set ) {
		$this->db->execute ( 'SET NAMES '. $set );
		return true;
	}

  /**
   * Last inserted id for auto_increment ID
   *
   * @return int
   * @throws PDOException
   * @access public
   */
	public function insert_id () {
		return $this->db->lastInsertId ();
	}

	/**
 	 * Escape and quote argument
 	 *
 	 * @param mixed $arg
 	 * @return mixed quoted argument
 	 * @throws PDOException
 	 * @access public
 	 */
	public function quote ( $arg ) {
		return $this->db->quote ( $arg );
	}

	/**
 	 * Reconnect to other database on same server with same login and password
 	 *
 	 * @param string database name
 	 * @return bool always true
 	 * @throws PDOException
 	 * @access public
 	 */
	public function database ( $dbname ) {
		$this->dbdata['name'] = $dbname;
		return $this->connect ();
	}

	/**
 	 * start transaction on rdbms where transactions works
 	 *
 	 * @return bool always true
 	 * @throws pdoexception
 	 * @access public
 	 */
	public function begin () {
		$this->db->beginTransaction ();
		return true;
	}

	/**
 	 * commit transaction on rdbms where transactions works
 	 *
 	 * @return bool always true
 	 * @throws pdoexception
 	 * @access public
 	 */
	public function commit () {
		$this->db->commit ();
		return true;
	}

	/**
 	 * rollback transaction on rdbms where transactions works
 	 *
 	 * @return bool always true
 	 * @throws pdoexception
 	 * @access public
 	 */
	public function rollback () {
		$this->db->rollBack ();
		return true;
	}

	/**
 	 * Returns PDO handler
 	 *
 	 * @return object PDO
 	 * @access public
 	 */
	public function get_pdo () {
		return $this->db;
	}

	/**
 	 * Execute query and store result in cache
 	 *
 	 * First of parameter must be SQL query, and next are arguments to bind to
 	 * query. Every argument os quoted in exactly same way as in
 	 * mPDOEasy::execute. Result is stored in mPDOEasy::cache, and where next
 	 * time is passed the same query with exactly the same parameters, there
 	 * will not be executed, but previously selected data will be returned.
 	 *
 	 * @param string $query
 	 * @param mixed values
 	 * @return array result set
 	 * @throws PDOException
 	 * @access public
 	 */
	public function cache () {
		$args = func_get_args ();

		# build key
		$key = array ();
		foreach ( $args as $arg ) {
			if ( is_array ( $arg ) ) {
				$key[] = $arg[0];
			}
			else {
				$key[] = $arg;
			}
		}
		$key = md5 ( join ( '|', $key) );

		# return resultset of exists
		if ( isset ( $this->cache[$key] ) ) {
			return $this->cache[$key];
		}

		# prepare and execute
		$this->prepare ( $args );

		# store in cache
		$this->cache[$key] = $this->cursor->fetchall ();
		$this->cursor->closeCursor ();

		return $this->cache[$key];
	}

	/**
 	 * Delete cached query from cache.
 	 *
 	 * @param string $query
 	 * @param mixed values
 	 * @return bool true if query was cached before, false in otherwise
 	 * @access public
 	 */
	public function invalidate () {
		$args = func_get_args ();

		# build key
		$key = array ();
		foreach ( $args as $arg ) {
			if ( is_array ( $arg ) ) {
				$key[] = $arg[0];
			}
			else {
				$key[] = $arg;
			}
		}
		$key = md5 ( join ( '|', $key ) );

		if ( isset ( $this->cache[$key] ) ) {
			unset ( $this->cache[$key] );
			return true;
		}

		return false;
	}

	/**
 	 * Switch and return current debug mode.
 	 *
 	 * If $mode will not be passed or null, current debug mode will be switched to
 	 * opposite. If true, it enable debug mode, if false, disable it.
 	 * Always return debug mode before change.
 	 *
 	 * Not implemented yet.
 	 *
 	 * @param bool $mode
 	 * @return bool
 	 * @access public
 	 */
	public function debug_mode ( $mode = null ) {
		$curr_debug_mode = $this->debug;

		if ( is_null ( $mode ) ) {
			$this->debug = !$this->debug;
		}
 	   	else {
			$this->debug = ( bool )$mode;
		}

		return $curr_debug_mode;
	}
}

?>
